package org.andengine.entity;

import org.andengine.engine.camera.Camera;
import org.andengine.engine.handler.IUpdateHandler;
import org.andengine.engine.handler.UpdateHandlerList;
import org.andengine.entity.modifier.EntityModifierList;
import org.andengine.entity.modifier.IEntityModifier;
import org.andengine.entity.modifier.IEntityModifier.IEntityModifierMatcher;
import org.andengine.opengl.util.GLState;
import org.andengine.util.Constants;
import org.andengine.util.adt.list.SmartList;
import org.andengine.util.adt.transformation.Transformation;
import org.andengine.util.call.ParameterCallable;
import org.andengine.util.color.Color;

import java.util.ArrayList;
import java.util.List;


/**
 * (c) 2010 Nicolas Gramlich
 * (c) 2011 Zynga Inc.
 *
 * @author Nicolas Gramlich
 * @since 12:00:48 - 08.03.2010
 */
public class Entity implements IEntity {
    // ===========================================================
    // Constants
    // ===========================================================

    private static final int CHILDREN_CAPACITY_DEFAULT = 4;
    private static final int ENTITYMODIFIERS_CAPACITY_DEFAULT = 4;
    private static final int UPDATEHANDLERS_CAPACITY_DEFAULT = 4;

    private static final float[] VERTICES_SCENE_TO_LOCAL_TMP = new float[2];
    private static final float[] VERTICES_LOCAL_TO_SCENE_TMP = new float[2];

    private static final ParameterCallable<IEntity> PARAMETERCALLABLE_DETACHCHILD = new ParameterCallable<IEntity>() {
        @Override
        public void call(final IEntity pEntity) {
            pEntity.setParent(null);
            pEntity.onDetached();
        }
    };

    // ===========================================================
    // Fields
    // ===========================================================

    protected boolean mDisposed;
    protected boolean mVisible = true;
    protected boolean mCullingEnabled;
    protected boolean mIgnoreUpdate;
    protected boolean mChildrenVisible = true;
    protected boolean mChildrenIgnoreUpdate;
    protected boolean mChildrenSortPending;

    protected int mTag = IEntity.TAG_INVALID;

    protected int mZIndex = 0;

    private IEntity mParent;

    protected SmartList<IEntity> mChildren;
    private EntityModifierList mEntityModifiers;
    private UpdateHandlerList mUpdateHandlers;

    protected Color mColor = new Color(1, 1, 1, 1);

    protected float mX;
    protected float mY;

    protected float mRotation = 0;

    protected float mRotationCenterX = 0;
    protected float mRotationCenterY = 0;

    protected float mScaleX = 1;
    protected float mScaleY = 1;

    protected float mScaleCenterX = 0;
    protected float mScaleCenterY = 0;

    protected float mSkewX = 0;
    protected float mSkewY = 0;

    protected float mSkewCenterX = 0;
    protected float mSkewCenterY = 0;

    private boolean mLocalToParentTransformationDirty = true;
    private boolean mParentToLocalTransformationDirty = true;

    private Transformation mLocalToParentTransformation;
    private Transformation mParentToLocalTransformation;

    private Transformation mLocalToSceneTransformation;
    private Transformation mSceneToLocalTransformation;

    private Object mUserData;

    // ===========================================================
    // Constructors
    // ===========================================================

    public Entity() {
        this(0, 0);
    }

    public Entity(final float pX, final float pY) {
        this.mX = pX;
        this.mY = pY;
    }

    // ===========================================================
    // Getter & Setter
    // ===========================================================

    // ===========================================================
    // Methods for/from SuperClass/Interfaces
    // ===========================================================

    protected void onUpdateColor() {

    }

    @Override
    public boolean isDisposed() {
        return this.mDisposed;
    }

    @Override
    public boolean isVisible() {
        return this.mVisible;
    }

    @Override
    public void setVisible(final boolean pVisible) {
        this.mVisible = pVisible;
    }

    @Override
    public boolean isCullingEnabled() {
        return this.mCullingEnabled;
    }

    @Override
    public void setCullingEnabled(final boolean pCullingEnabled) {
        this.mCullingEnabled = pCullingEnabled;
    }

    @Override
    public boolean isCulled(final Camera pCamera) {
        return false;
    }

    @Override
    public boolean isChildrenVisible() {
        return this.mChildrenVisible;
    }

    @Override
    public void setChildrenVisible(final boolean pChildrenVisible) {
        this.mChildrenVisible = pChildrenVisible;
    }

    @Override
    public boolean isIgnoreUpdate() {
        return this.mIgnoreUpdate;
    }

    @Override
    public void setIgnoreUpdate(final boolean pIgnoreUpdate) {
        this.mIgnoreUpdate = pIgnoreUpdate;
    }

    @Override
    public boolean isChildrenIgnoreUpdate() {
        return this.mChildrenIgnoreUpdate;
    }

    @Override
    public void setChildrenIgnoreUpdate(final boolean pChildrenIgnoreUpdate) {
        this.mChildrenIgnoreUpdate = pChildrenIgnoreUpdate;
    }

    @Override
    public boolean hasParent() {
        return this.mParent != null;
    }

    @Override
    public IEntity getParent() {
        return this.mParent;
    }

    @Override
    public void setParent(final IEntity pEntity) {
        this.mParent = pEntity;
    }

    @Override
    public int getTag() {
        return this.mTag;
    }

    @Override
    public void setTag(final int pTag) {
        this.mTag = pTag;
    }

    @Override
    public int getZIndex() {
        return this.mZIndex;
    }

    @Override
    public void setZIndex(final int pZIndex) {
        this.mZIndex = pZIndex;
    }

    @Override
    public float getX() {
        return this.mX;
    }

    @Override
    public float getY() {
        return this.mY;
    }

    @Override
    public void setX(final float pX) {
        this.mX = pX;

        this.mLocalToParentTransformationDirty = true;
        this.mParentToLocalTransformationDirty = true;
    }

    @Override
    public void setY(final float pY) {
        this.mY = pY;

        this.mLocalToParentTransformationDirty = true;
        this.mParentToLocalTransformationDirty = true;
    }

    @Override
    public void setPosition(final IEntity pOtherEntity) {
        this.setPosition(pOtherEntity.getX(), pOtherEntity.getY());
    }

    @Override
    public void setPosition(final float pX, final float pY) {
        this.mX = pX;
        this.mY = pY;

        this.mLocalToParentTransformationDirty = true;
        this.mParentToLocalTransformationDirty = true;
    }

    @Override
    public float getRotation() {
        return this.mRotation;
    }

    @Override
    public boolean isRotated() {
        return this.mRotation != 0;
    }

    @Override
    public void setRotation(final float pRotation) {
        this.mRotation = pRotation;

        this.mLocalToParentTransformationDirty = true;
        this.mParentToLocalTransformationDirty = true;
    }

    @Override
    public float getRotationCenterX() {
        return this.mRotationCenterX;
    }

    @Override
    public float getRotationCenterY() {
        return this.mRotationCenterY;
    }

    @Override
    public void setRotationCenterX(final float pRotationCenterX) {
        this.mRotationCenterX = pRotationCenterX;

        this.mLocalToParentTransformationDirty = true;
        this.mParentToLocalTransformationDirty = true;
    }

    @Override
    public void setRotationCenterY(final float pRotationCenterY) {
        this.mRotationCenterY = pRotationCenterY;

        this.mLocalToParentTransformationDirty = true;
        this.mParentToLocalTransformationDirty = true;
    }

    @Override
    public void setRotationCenter(final float pRotationCenterX, final float pRotationCenterY) {
        this.mRotationCenterX = pRotationCenterX;
        this.mRotationCenterY = pRotationCenterY;

        this.mLocalToParentTransformationDirty = true;
        this.mParentToLocalTransformationDirty = true;
    }

    @Override
    public boolean isScaled() {
        return (this.mScaleX != 1) || (this.mScaleY != 1);
    }

    @Override
    public float getScaleX() {
        return this.mScaleX;
    }

    @Override
    public float getScaleY() {
        return this.mScaleY;
    }

    @Override
    public void setScaleX(final float pScaleX) {
        this.mScaleX = pScaleX;

        this.mLocalToParentTransformationDirty = true;
        this.mParentToLocalTransformationDirty = true;
    }

    @Override
    public void setScaleY(final float pScaleY) {
        this.mScaleY = pScaleY;

        this.mLocalToParentTransformationDirty = true;
        this.mParentToLocalTransformationDirty = true;
    }

    @Override
    public void setScale(final float pScale) {
        this.mScaleX = pScale;
        this.mScaleY = pScale;

        this.mLocalToParentTransformationDirty = true;
        this.mParentToLocalTransformationDirty = true;
    }

    @Override
    public void setScale(final float pScaleX, final float pScaleY) {
        this.mScaleX = pScaleX;
        this.mScaleY = pScaleY;

        this.mLocalToParentTransformationDirty = true;
        this.mParentToLocalTransformationDirty = true;
    }

    @Override
    public float getScaleCenterX() {
        return this.mScaleCenterX;
    }

    @Override
    public float getScaleCenterY() {
        return this.mScaleCenterY;
    }

    @Override
    public void setScaleCenterX(final float pScaleCenterX) {
        this.mScaleCenterX = pScaleCenterX;

        this.mLocalToParentTransformationDirty = true;
        this.mParentToLocalTransformationDirty = true;
    }

    @Override
    public void setScaleCenterY(final float pScaleCenterY) {
        this.mScaleCenterY = pScaleCenterY;

        this.mLocalToParentTransformationDirty = true;
        this.mParentToLocalTransformationDirty = true;
    }

    @Override
    public void setScaleCenter(final float pScaleCenterX, final float pScaleCenterY) {
        this.mScaleCenterX = pScaleCenterX;
        this.mScaleCenterY = pScaleCenterY;

        this.mLocalToParentTransformationDirty = true;
        this.mParentToLocalTransformationDirty = true;
    }

    @Override
    public boolean isSkewed() {
        return (this.mSkewX != 0) || (this.mSkewY != 0);
    }

    @Override
    public float getSkewX() {
        return this.mSkewX;
    }

    @Override
    public float getSkewY() {
        return this.mSkewY;
    }

    @Override
    public void setSkewX(final float pSkewX) {
        this.mSkewX = pSkewX;

        this.mLocalToParentTransformationDirty = true;
        this.mParentToLocalTransformationDirty = true;
    }

    @Override
    public void setSkewY(final float pSkewY) {
        this.mSkewY = pSkewY;

        this.mLocalToParentTransformationDirty = true;
        this.mParentToLocalTransformationDirty = true;
    }

    @Override
    public void setSkew(final float pSkew) {
        this.mSkewX = pSkew;
        this.mSkewY = pSkew;

        this.mLocalToParentTransformationDirty = true;
        this.mParentToLocalTransformationDirty = true;
    }

    @Override
    public void setSkew(final float pSkewX, final float pSkewY) {
        this.mSkewX = pSkewX;
        this.mSkewY = pSkewY;

        this.mLocalToParentTransformationDirty = true;
        this.mParentToLocalTransformationDirty = true;
    }

    @Override
    public float getSkewCenterX() {
        return this.mSkewCenterX;
    }

    @Override
    public float getSkewCenterY() {
        return this.mSkewCenterY;
    }

    @Override
    public void setSkewCenterX(final float pSkewCenterX) {
        this.mSkewCenterX = pSkewCenterX;

        this.mLocalToParentTransformationDirty = true;
        this.mParentToLocalTransformationDirty = true;
    }

    @Override
    public void setSkewCenterY(final float pSkewCenterY) {
        this.mSkewCenterY = pSkewCenterY;

        this.mLocalToParentTransformationDirty = true;
        this.mParentToLocalTransformationDirty = true;
    }

    @Override
    public void setSkewCenter(final float pSkewCenterX, final float pSkewCenterY) {
        this.mSkewCenterX = pSkewCenterX;
        this.mSkewCenterY = pSkewCenterY;

        this.mLocalToParentTransformationDirty = true;
        this.mParentToLocalTransformationDirty = true;
    }

    @Override
    public boolean isRotatedOrScaledOrSkewed() {
        return (this.mRotation != 0) || (this.mScaleX != 1) || (this.mScaleY != 1) || (this.mSkewX != 0) || (this.mSkewY != 0);
    }

    @Override
    public float getRed() {
        return this.mColor.getRed();
    }

    @Override
    public float getGreen() {
        return this.mColor.getGreen();
    }

    @Override
    public float getBlue() {
        return this.mColor.getBlue();
    }

    @Override
    public float getAlpha() {
        return this.mColor.getAlpha();
    }

    @Override
    public Color getColor() {
        return this.mColor;
    }

    @Override
    public void setColor(final Color pColor) {
        this.mColor.set(pColor);

        this.onUpdateColor();
    }

    /**
     * @param pRed from <code>0.0f</code> to <code>1.0f</code>
     */
    @Override
    public void setRed(final float pRed) {
        if (this.mColor.setRedChecking(pRed)) {
            this.onUpdateColor();
        }
    }

    /**
     * @param pGreen from <code>0.0f</code> to <code>1.0f</code>
     */
    @Override
    public void setGreen(final float pGreen) {
        if (this.mColor.setGreenChecking(pGreen)) {
            this.onUpdateColor();
        }
    }

    /**
     * @param pBlue from <code>0.0f</code> to <code>1.0f</code>
     */
    @Override
    public void setBlue(final float pBlue) {
        if (this.mColor.setBlueChecking(pBlue)) {
            this.onUpdateColor();
        }
    }

    /**
     * @param pAlpha from <code>0.0f</code> (transparent) to <code>1.0f</code> (opaque)
     */
    @Override
    public void setAlpha(final float pAlpha) {
        if (this.mColor.setAlphaChecking(pAlpha)) {
            this.onUpdateColor();
        }
    }

    /**
     * @param pRed   from <code>0.0f</code> to <code>1.0f</code>
     * @param pGreen from <code>0.0f</code> to <code>1.0f</code>
     * @param pBlue  from <code>0.0f</code> to <code>1.0f</code>
     */
    @Override
    public void setColor(final float pRed, final float pGreen, final float pBlue) {
        if (this.mColor.setChecking(pRed, pGreen, pBlue)) { // TODO Is this check worth it?
            this.onUpdateColor();
        }
    }

    /**
     * @param pRed   from <code>0.0f</code> to <code>1.0f</code>
     * @param pGreen from <code>0.0f</code> to <code>1.0f</code>
     * @param pBlue  from <code>0.0f</code> to <code>1.0f</code>
     * @param pAlpha from <code>0.0f</code> (transparent) to <code>1.0f</code> (opaque)
     */
    @Override
    public void setColor(final float pRed, final float pGreen, final float pBlue, final float pAlpha) {
        if (this.mColor.setChecking(pRed, pGreen, pBlue, pAlpha)) { // TODO Is this check worth it?
            this.onUpdateColor();
        }
    }

    @Override
    public int getChildCount() {
        if (this.mChildren == null) {
            return 0;
        }
        return this.mChildren.size();
    }

    @Override
    public IEntity getChildByTag(final int pTag) {
        if (this.mChildren == null) {
            return null;
        }
        for (int i = this.mChildren.size() - 1; i >= 0; i--) {
            final IEntity child = this.mChildren.get(i);
            if (child.getTag() == pTag) {
                return child;
            }
        }
        return null;
    }

    @Override
    public IEntity getChildByIndex(final int pIndex) {
        if (this.mChildren == null) {
            return null;
        }
        return this.mChildren.get(pIndex);
    }

    @Override
    public IEntity getChildByMatcher(final IEntityMatcher pEntityMatcher) {
        if (this.mChildren == null) {
            return null;
        }
        return this.mChildren.get(pEntityMatcher);
    }

    @Override
    public IEntity getFirstChild() {
        if (this.mChildren == null) {
            return null;
        }
        return this.mChildren.get(0);
    }

    @Override
    public IEntity getLastChild() {
        if (this.mChildren == null) {
            return null;
        }
        return this.mChildren.get(this.mChildren.size() - 1);
    }

    @Override
    public ArrayList<IEntity> query(final IEntityMatcher pEntityMatcher) {
        return this.query(pEntityMatcher, new ArrayList<IEntity>());
    }

    @Override
    public IEntity queryFirst(final IEntityMatcher pEntityMatcher) {
        return this.queryFirstForSubclass(pEntityMatcher);
    }

    @SuppressWarnings("unchecked")
    @Override
    public <S extends IEntity> S queryFirstForSubclass(final IEntityMatcher pEntityMatcher) {
        final int childCount = this.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final IEntity child = this.mChildren.get(i);
            if (pEntityMatcher.matches(child)) {
                return (S) child;
            }

            final S childQueryFirst = child.queryFirstForSubclass(pEntityMatcher);
            if (childQueryFirst != null) {
                return childQueryFirst;
            }
        }

        return null;
    }

    @Override
    public <L extends List<IEntity>> L query(final IEntityMatcher pEntityMatcher, final L pResult) {
        final int childCount = this.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final IEntity child = this.mChildren.get(i);
            if (pEntityMatcher.matches(child)) {
                pResult.add(child);
            }

            child.query(pEntityMatcher, pResult);
        }

        return pResult;
    }

    @Override
    public <S extends IEntity> ArrayList<S> queryForSubclass(final IEntityMatcher pEntityMatcher) throws ClassCastException {
        return this.queryForSubclass(pEntityMatcher, new ArrayList<S>());
    }

    @SuppressWarnings("unchecked")
    @Override
    public <L extends List<S>, S extends IEntity> L queryForSubclass(final IEntityMatcher pEntityMatcher, final L pResult) throws ClassCastException {
        final int childCount = this.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final IEntity child = this.mChildren.get(i);
            if (pEntityMatcher.matches(child)) {
                pResult.add((S) child);
            }

            child.queryForSubclass(pEntityMatcher, pResult);
        }

        return pResult;
    }

    @Override
    public boolean detachSelf() {
        final IEntity parent = this.mParent;
        if (parent != null) {
            return parent.detachChild(this);
        } else {
            return false;
        }
    }

    @Override
    public void detachChildren() {
        if (this.mChildren == null) {
            return;
        }
        this.mChildren.clear(Entity.PARAMETERCALLABLE_DETACHCHILD);
    }

    @Override
    public void attachChild(final IEntity pEntity) throws IllegalStateException {
        this.assertEntityHasNoParent(pEntity);

        if (this.mChildren == null) {
            this.allocateChildren();
        }
        this.mChildren.add(pEntity);
        pEntity.setParent(this);
        pEntity.onAttached();
    }

    @Override
    public void sortChildren() {
        this.sortChildren(true);
    }

    @Override
    public void sortChildren(final boolean pImmediate) {
        if (this.mChildren == null) {
            return;
        }
        if (pImmediate) {
            ZIndexSorter.getInstance().sort(this.mChildren);
        } else {
            this.mChildrenSortPending = true;
        }
    }

    @Override
    public void sortChildren(final IEntityComparator pEntityComparator) {
        if (this.mChildren == null) {
            return;
        }
        ZIndexSorter.getInstance().sort(this.mChildren, pEntityComparator);
    }

    @Override
    public boolean detachChild(final IEntity pEntity) {
        if (this.mChildren == null) {
            return false;
        }
        return this.mChildren.remove(pEntity, Entity.PARAMETERCALLABLE_DETACHCHILD);
    }

    @Override
    public IEntity detachChild(final int pTag) {
        if (this.mChildren == null) {
            return null;
        }
        for (int i = this.mChildren.size() - 1; i >= 0; i--) {
            if (this.mChildren.get(i).getTag() == pTag) {
                final IEntity removed = this.mChildren.remove(i);
                Entity.PARAMETERCALLABLE_DETACHCHILD.call(removed);
                return removed;
            }
        }
        return null;
    }

    @Override
    public IEntity detachChild(final IEntityMatcher pEntityMatcher) {
        if (this.mChildren == null) {
            return null;
        }
        return this.mChildren.remove(pEntityMatcher, Entity.PARAMETERCALLABLE_DETACHCHILD);
    }

    @Override
    public boolean detachChildren(final IEntityMatcher pEntityMatcher) {
        if (this.mChildren == null) {
            return false;
        }
        return this.mChildren.removeAll(pEntityMatcher, Entity.PARAMETERCALLABLE_DETACHCHILD);
    }

    @Override
    public void callOnChildren(final IEntityParameterCallable pEntityParameterCallable) {
        if (this.mChildren == null) {
            return;
        }
        this.mChildren.call(pEntityParameterCallable);
    }

    @Override
    public void callOnChildren(final IEntityParameterCallable pEntityParameterCallable, final IEntityMatcher pEntityMatcher) {
        if (this.mChildren == null) {
            return;
        }
        this.mChildren.call(pEntityMatcher, pEntityParameterCallable);
    }

    @Override
    public void registerUpdateHandler(final IUpdateHandler pUpdateHandler) {
        if (this.mUpdateHandlers == null) {
            this.allocateUpdateHandlers();
        }
        this.mUpdateHandlers.add(pUpdateHandler);
    }

    @Override
    public boolean unregisterUpdateHandler(final IUpdateHandler pUpdateHandler) {
        if (this.mUpdateHandlers == null) {
            return false;
        }
        return this.mUpdateHandlers.remove(pUpdateHandler);
    }

    @Override
    public boolean unregisterUpdateHandlers(final IUpdateHandlerMatcher pUpdateHandlerMatcher) {
        if (this.mUpdateHandlers == null) {
            return false;
        }
        return this.mUpdateHandlers.removeAll(pUpdateHandlerMatcher);
    }

    @Override
    public int getUpdateHandlerCount() {
        if (this.mUpdateHandlers == null) {
            return 0;
        }
        return this.mUpdateHandlers.size();
    }

    @Override
    public void clearUpdateHandlers() {
        if (this.mUpdateHandlers == null) {
            return;
        }
        this.mUpdateHandlers.clear();
    }

    @Override
    public void registerEntityModifier(final IEntityModifier pEntityModifier) {
        if (this.mEntityModifiers == null) {
            this.allocateEntityModifiers();
        }
        this.mEntityModifiers.add(pEntityModifier);
    }

    @Override
    public boolean unregisterEntityModifier(final IEntityModifier pEntityModifier) {
        if (this.mEntityModifiers == null) {
            return false;
        }
        return this.mEntityModifiers.remove(pEntityModifier);
    }

    @Override
    public boolean unregisterEntityModifiers(final IEntityModifierMatcher pEntityModifierMatcher) {
        if (this.mEntityModifiers == null) {
            return false;
        }
        return this.mEntityModifiers.removeAll(pEntityModifierMatcher);
    }

    @Override
    public int getEntityModifierCount() {
        if (this.mEntityModifiers == null) {
            return 0;
        }
        return this.mEntityModifiers.size();
    }

    @Override
    public void clearEntityModifiers() {
        if (this.mEntityModifiers == null) {
            return;
        }
        this.mEntityModifiers.clear();
    }

    @Override
    public float[] getSceneCenterCoordinates() {
        return this.convertLocalToSceneCoordinates(0, 0);
    }

    @Override
    public float[] getSceneCenterCoordinates(final float[] pReuse) {
        return this.convertLocalToSceneCoordinates(0, 0, pReuse);
    }

    @Override
    public Transformation getLocalToParentTransformation() {
        if (this.mLocalToParentTransformation == null) {
            this.mLocalToParentTransformation = new Transformation();
        }

        final Transformation localToParentTransformation = this.mLocalToParentTransformation;
        if (this.mLocalToParentTransformationDirty) {
            localToParentTransformation.setToIdentity();

			/* Scale. */
            final float scaleX = this.mScaleX;
            final float scaleY = this.mScaleY;
            if ((scaleX != 1) || (scaleY != 1)) {
                final float scaleCenterX = this.mScaleCenterX;
                final float scaleCenterY = this.mScaleCenterY;

				/* TODO Check if it is worth to check for scaleCenterX == 0 && scaleCenterY == 0 as the two postTranslate can be saved.
                 * The same obviously applies for all similar occurrences of this pattern in this class. */

                localToParentTransformation.postTranslate(-scaleCenterX, -scaleCenterY);
                localToParentTransformation.postScale(scaleX, scaleY);
                localToParentTransformation.postTranslate(scaleCenterX, scaleCenterY);
            }

			/* Skew. */
            final float skewX = this.mSkewX;
            final float skewY = this.mSkewY;
            if ((skewX != 0) || (skewY != 0)) {
                final float skewCenterX = this.mSkewCenterX;
                final float skewCenterY = this.mSkewCenterY;

                localToParentTransformation.postTranslate(-skewCenterX, -skewCenterY);
                localToParentTransformation.postSkew(skewX, skewY);
                localToParentTransformation.postTranslate(skewCenterX, skewCenterY);
            }

			/* Rotation. */
            final float rotation = this.mRotation;
            if (rotation != 0) {
                final float rotationCenterX = this.mRotationCenterX;
                final float rotationCenterY = this.mRotationCenterY;

                localToParentTransformation.postTranslate(-rotationCenterX, -rotationCenterY);
                localToParentTransformation.postRotate(rotation);
                localToParentTransformation.postTranslate(rotationCenterX, rotationCenterY);
            }

			/* Translation. */
            localToParentTransformation.postTranslate(this.mX, this.mY);

            this.mLocalToParentTransformationDirty = false;
        }
        return localToParentTransformation;
    }

    @Override
    public Transformation getParentToLocalTransformation() {
        if (this.mParentToLocalTransformation == null) {
            this.mParentToLocalTransformation = new Transformation();
        }

        final Transformation parentToLocalTransformation = this.mParentToLocalTransformation;
        if (this.mParentToLocalTransformationDirty) {
            parentToLocalTransformation.setToIdentity();

			/* Translation. */
            parentToLocalTransformation.postTranslate(-this.mX, -this.mY);

			/* Rotation. */
            final float rotation = this.mRotation;
            if (rotation != 0) {
                final float rotationCenterX = this.mRotationCenterX;
                final float rotationCenterY = this.mRotationCenterY;

                parentToLocalTransformation.postTranslate(-rotationCenterX, -rotationCenterY);
                parentToLocalTransformation.postRotate(-rotation);
                parentToLocalTransformation.postTranslate(rotationCenterX, rotationCenterY);
            }

			/* Skew. */
            final float skewX = this.mSkewX;
            final float skewY = this.mSkewY;
            if ((skewX != 0) || (skewY != 0)) {
                final float skewCenterX = this.mSkewCenterX;
                final float skewCenterY = this.mSkewCenterY;

                parentToLocalTransformation.postTranslate(-skewCenterX, -skewCenterY);
                parentToLocalTransformation.postSkew(-skewX, -skewY);
                parentToLocalTransformation.postTranslate(skewCenterX, skewCenterY);
            }

			/* Scale. */
            final float scaleX = this.mScaleX;
            final float scaleY = this.mScaleY;
            if ((scaleX != 1) || (scaleY != 1)) {
                final float scaleCenterX = this.mScaleCenterX;
                final float scaleCenterY = this.mScaleCenterY;

                parentToLocalTransformation.postTranslate(-scaleCenterX, -scaleCenterY);
                parentToLocalTransformation.postScale(1 / scaleX, 1 / scaleY); // TODO Division could be replaced by a multiplication of 'scale(X/Y)Inverse'...
                parentToLocalTransformation.postTranslate(scaleCenterX, scaleCenterY);
            }

            this.mParentToLocalTransformationDirty = false;
        }
        return parentToLocalTransformation;
    }

    @Override
    public Transformation getLocalToSceneTransformation() {
        if (this.mLocalToSceneTransformation == null) {
            this.mLocalToSceneTransformation = new Transformation();
        }

        // TODO Cache if parent(recursive) not dirty.
        final Transformation localToSceneTransformation = this.mLocalToSceneTransformation;
        localToSceneTransformation.setTo(this.getLocalToParentTransformation());

        final IEntity parent = this.mParent;
        if (parent != null) {
            localToSceneTransformation.postConcat(parent.getLocalToSceneTransformation());
        }

        return localToSceneTransformation;
    }

    @Override
    public Transformation getSceneToLocalTransformation() {
        if (this.mSceneToLocalTransformation == null) {
            this.mSceneToLocalTransformation = new Transformation();
        }

        // TODO Cache if parent(recursive) not dirty.
        final Transformation sceneToLocalTransformation = this.mSceneToLocalTransformation;
        sceneToLocalTransformation.setTo(this.getParentToLocalTransformation());

        final IEntity parent = this.mParent;
        if (parent != null) {
            sceneToLocalTransformation.preConcat(parent.getSceneToLocalTransformation());
        }

        return sceneToLocalTransformation;
    }

    /* (non-Javadoc)
     * @see org.andengine.entity.IEntity#convertLocalToSceneCoordinates(float, float)
     */
    @Override
    public float[] convertLocalToSceneCoordinates(final float pX, final float pY) {
        return this.convertLocalToSceneCoordinates(pX, pY, Entity.VERTICES_LOCAL_TO_SCENE_TMP);
    }

    /* (non-Javadoc)
     * @see org.andengine.entity.IEntity#convertLocalToSceneCoordinates(float, float, float[])
     */
    @Override
    public float[] convertLocalToSceneCoordinates(final float pX, final float pY, final float[] pReuse) {
        final Transformation localToSceneTransformation = this.getLocalToSceneTransformation();

        pReuse[Constants.VERTEX_INDEX_X] = pX;
        pReuse[Constants.VERTEX_INDEX_Y] = pY;

        localToSceneTransformation.transform(pReuse);

        return pReuse;
    }

    /* (non-Javadoc)
     * @see org.andengine.entity.IEntity#convertLocalToSceneCoordinates(float[])
     */
    @Override
    public float[] convertLocalToSceneCoordinates(final float[] pCoordinates) {
        return this.convertLocalToSceneCoordinates(pCoordinates, Entity.VERTICES_LOCAL_TO_SCENE_TMP);
    }

    /* (non-Javadoc)
     * @see org.andengine.entity.IEntity#convertLocalToSceneCoordinates(float[], float[])
     */
    @Override
    public float[] convertLocalToSceneCoordinates(final float[] pCoordinates, final float[] pReuse) {
        final Transformation localToSceneTransformation = this.getLocalToSceneTransformation();

        pReuse[Constants.VERTEX_INDEX_X] = pCoordinates[Constants.VERTEX_INDEX_X];
        pReuse[Constants.VERTEX_INDEX_Y] = pCoordinates[Constants.VERTEX_INDEX_Y];

        localToSceneTransformation.transform(pReuse);

        return pReuse;
    }

    /* (non-Javadoc)
     * @see org.andengine.entity.IEntity#convertSceneToLocalCoordinates(float, float)
     */
    @Override
    public float[] convertSceneToLocalCoordinates(final float pX, final float pY) {
        return this.convertSceneToLocalCoordinates(pX, pY, Entity.VERTICES_SCENE_TO_LOCAL_TMP);
    }

    /* (non-Javadoc)
     * @see org.andengine.entity.IEntity#convertSceneToLocalCoordinates(float, float, float[])
     */
    @Override
    public float[] convertSceneToLocalCoordinates(final float pX, final float pY, final float[] pReuse) {
        pReuse[Constants.VERTEX_INDEX_X] = pX;
        pReuse[Constants.VERTEX_INDEX_Y] = pY;

        this.getSceneToLocalTransformation().transform(pReuse);

        return pReuse;
    }

    /* (non-Javadoc)
     * @see org.andengine.entity.IEntity#convertSceneToLocalCoordinates(float[])
     */
    @Override
    public float[] convertSceneToLocalCoordinates(final float[] pCoordinates) {
        return this.convertSceneToLocalCoordinates(pCoordinates, Entity.VERTICES_SCENE_TO_LOCAL_TMP);
    }

    /* (non-Javadoc)
     * @see org.andengine.entity.IEntity#convertSceneToLocalCoordinates(float[], float[])
     */
    @Override
    public float[] convertSceneToLocalCoordinates(final float[] pCoordinates, final float[] pReuse) {
        pReuse[Constants.VERTEX_INDEX_X] = pCoordinates[Constants.VERTEX_INDEX_X];
        pReuse[Constants.VERTEX_INDEX_Y] = pCoordinates[Constants.VERTEX_INDEX_Y];

        this.getSceneToLocalTransformation().transform(pReuse);

        return pReuse;
    }

    @Override
    public void onAttached() {

    }

    @Override
    public void onDetached() {

    }

    @Override
    public Object getUserData() {
        return this.mUserData;
    }

    @Override
    public void setUserData(final Object pUserData) {
        this.mUserData = pUserData;
    }

    @Override
    public final void onDraw(final GLState pGLState, final Camera pCamera) {
        if (this.mVisible && !(this.mCullingEnabled && this.isCulled(pCamera))) {
            this.onManagedDraw(pGLState, pCamera);
        }
    }

    @Override
    public final void onUpdate(final float pSecondsElapsed) {
        if (!this.mIgnoreUpdate) {
            this.onManagedUpdate(pSecondsElapsed);
        }
    }

    @Override
    public void reset() {
        this.mVisible = true;
        this.mCullingEnabled = false;
        this.mIgnoreUpdate = false;
        this.mChildrenVisible = true;
        this.mChildrenIgnoreUpdate = false;

        this.mRotation = 0;
        this.mScaleX = 1;
        this.mScaleY = 1;
        this.mSkewX = 0;
        this.mSkewY = 0;

        this.mColor.reset();

        if (this.mEntityModifiers != null) {
            this.mEntityModifiers.reset();
        }

        if (this.mChildren != null) {
            final SmartList<IEntity> entities = this.mChildren;
            for (int i = entities.size() - 1; i >= 0; i--) {
                entities.get(i).reset();
            }
        }
    }

    @Override
    public void dispose() {
        if (!this.mDisposed) {
            this.mDisposed = true;
        } else {
            throw new AlreadyDisposedException();
        }
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();

        if (!this.mDisposed) {
            this.dispose();
        }
    }

    @Override
    public String toString() {
        final StringBuilder stringBuilder = new StringBuilder();
        this.toString(stringBuilder);
        return stringBuilder.toString();
    }

    @Override
    public void toString(final StringBuilder pStringBuilder) {
        pStringBuilder.append(this.getClass().getSimpleName());

        if ((this.mChildren != null) && (this.mChildren.size() > 0)) {
            pStringBuilder.append(" [");
            final SmartList<IEntity> entities = this.mChildren;
            for (int i = 0; i < entities.size(); i++) {
                entities.get(i).toString(pStringBuilder);
                if (i < (entities.size() - 1)) {
                    pStringBuilder.append(", ");
                }
            }
            pStringBuilder.append("]");
        }
    }

    // ===========================================================
    // Methods
    // ===========================================================

    /**
     * @param pGLState the currently active {@link GLState} i.e. to apply transformations to.
     * @param pCamera  the currently active {@link Camera} i.e. to be used for culling.
     */
    protected void preDraw(final GLState pGLState, final Camera pCamera) {

    }

    /**
     * @param pGLState the currently active {@link GLState} i.e. to apply transformations to.
     * @param pCamera  the currently active {@link Camera} i.e. to be used for culling.
     */
    protected void draw(final GLState pGLState, final Camera pCamera) {

    }

    /**
     * @param pGLState the currently active {@link GLState} i.e. to apply transformations to.
     * @param pCamera  the currently active {@link Camera} i.e. to be used for culling.
     */
    protected void postDraw(final GLState pGLState, final Camera pCamera) {

    }

    private void allocateEntityModifiers() {
        this.mEntityModifiers = new EntityModifierList(this, Entity.ENTITYMODIFIERS_CAPACITY_DEFAULT);
    }

    private void allocateChildren() {
        this.mChildren = new SmartList<IEntity>(Entity.CHILDREN_CAPACITY_DEFAULT);
    }

    private void allocateUpdateHandlers() {
        this.mUpdateHandlers = new UpdateHandlerList(Entity.UPDATEHANDLERS_CAPACITY_DEFAULT);
    }

    protected void onApplyTransformations(final GLState pGLState) {
        /* Translation. */
        this.applyTranslation(pGLState);

		/* Rotation. */
        this.applyRotation(pGLState);

		/* Skew. */
        this.applySkew(pGLState);

		/* Scale. */
        this.applyScale(pGLState);
    }

    protected void applyTranslation(final GLState pGLState) {
        pGLState.translateModelViewGLMatrixf(this.mX, this.mY, 0);
    }

    protected void applyRotation(final GLState pGLState) {
        final float rotation = this.mRotation;

        if (rotation != 0) {
            final float rotationCenterX = this.mRotationCenterX;
            final float rotationCenterY = this.mRotationCenterY;

            pGLState.translateModelViewGLMatrixf(rotationCenterX, rotationCenterY, 0);
            pGLState.rotateModelViewGLMatrixf(rotation, 0, 0, 1);
            pGLState.translateModelViewGLMatrixf(-rotationCenterX, -rotationCenterY, 0);

			/* TODO There is a special, but very likely case when mRotationCenter and mScaleCenter are the same.
             * In that case the last glTranslatef of the rotation and the first glTranslatef of the scale is superfluous.
			 * The problem is that applyRotation and applyScale would need to be "merged" in order to efficiently check for that condition.  */
        }
    }

    protected void applySkew(final GLState pGLState) {
        final float skewX = this.mSkewX;
        final float skewY = this.mSkewY;

        if ((skewX != 0) || (skewY != 0)) {
            final float skewCenterX = this.mSkewCenterX;
            final float skewCenterY = this.mSkewCenterY;

            pGLState.translateModelViewGLMatrixf(skewCenterX, skewCenterY, 0);
            pGLState.skewModelViewGLMatrixf(skewX, skewY);
            pGLState.translateModelViewGLMatrixf(-skewCenterX, -skewCenterY, 0);
        }
    }

    protected void applyScale(final GLState pGLState) {
        final float scaleX = this.mScaleX;
        final float scaleY = this.mScaleY;

        if ((scaleX != 1) || (scaleY != 1)) {
            final float scaleCenterX = this.mScaleCenterX;
            final float scaleCenterY = this.mScaleCenterY;

            pGLState.translateModelViewGLMatrixf(scaleCenterX, scaleCenterY, 0);
            pGLState.scaleModelViewGLMatrixf(scaleX, scaleY, 1);
            pGLState.translateModelViewGLMatrixf(-scaleCenterX, -scaleCenterY, 0);
        }
    }

    protected void onManagedDraw(final GLState pGLState, final Camera pCamera) {
        pGLState.pushModelViewGLMatrix();
        {
            this.onApplyTransformations(pGLState);

            final SmartList<IEntity> children = this.mChildren;
            if ((children == null) || !this.mChildrenVisible) {
				/* Draw only self. */
                this.preDraw(pGLState, pCamera);
                this.draw(pGLState, pCamera);
                this.postDraw(pGLState, pCamera);
            } else {
                if (this.mChildrenSortPending) {
                    ZIndexSorter.getInstance().sort(this.mChildren);
                    this.mChildrenSortPending = false;
                }

                final int childCount = children.size();
                int i = 0;

                { /* Draw children behind this Entity. */
                    for (; i < childCount; i++) {
                        final IEntity child = children.get(i);
                        if (child.getZIndex() < 0) {
                            child.onDraw(pGLState, pCamera);
                        } else {
                            break;
                        }
                    }
                }

				/* Draw self. */
                this.preDraw(pGLState, pCamera);
                this.draw(pGLState, pCamera);
                this.postDraw(pGLState, pCamera);

                { /* Draw children in front of this Entity. */
                    for (; i < childCount; i++) {
                        children.get(i).onDraw(pGLState, pCamera);
                    }
                }
            }
        }
        pGLState.popModelViewGLMatrix();
    }

    protected void onManagedUpdate(final float pSecondsElapsed) {
        if (this.mEntityModifiers != null) {
            this.mEntityModifiers.onUpdate(pSecondsElapsed);
        }
        if (this.mUpdateHandlers != null) {
            this.mUpdateHandlers.onUpdate(pSecondsElapsed);
        }

        if ((this.mChildren != null) && !this.mChildrenIgnoreUpdate) {
            final SmartList<IEntity> entities = this.mChildren;
            final int entityCount = entities.size();
            for (int i = 0; i < entityCount; i++) {
                entities.get(i).onUpdate(pSecondsElapsed);
            }
        }
    }

    private void assertEntityHasNoParent(final IEntity pEntity) throws IllegalStateException {
        if (pEntity.hasParent()) {
            final String entityClassName = pEntity.getClass().getSimpleName();
            final String currentParentClassName = pEntity.getParent().getClass().getSimpleName();
            final String newParentClassName = this.getClass().getSimpleName();
            throw new IllegalStateException("pEntity '" + entityClassName + "' already has a parent: '" + currentParentClassName + "'. New parent: '" + newParentClassName + "'!");
        }
    }

    // ===========================================================
    // Inner and Anonymous Classes
    // ===========================================================
}
